package cn.kinoko.service.manga.impl;

import cn.kinoko.common.aspect.annotation.CacheEvict;
import cn.kinoko.common.aspect.annotation.Cacheable;
import cn.kinoko.common.base.model.BaseQuery;
import cn.kinoko.common.base.model.BaseServiceImpl;
import cn.kinoko.common.code.ErrorCodeEnum;
import cn.kinoko.common.constant.CacheKeyConstant;
import cn.kinoko.common.constant.Flag;
import cn.kinoko.common.model.KV;
import cn.kinoko.common.utils.*;
import cn.kinoko.mapper.manga.MangaMapper;
import cn.kinoko.model.cnTeam.constant.Position;
import cn.kinoko.model.cnTeam.constant.PositionType;
import cn.kinoko.model.cnTeam.entity.CnTeam;
import cn.kinoko.model.cnTeam.entity.CnTeamMember;
import cn.kinoko.model.cnTeam.vo.MemberVO;
import cn.kinoko.model.manga.constant.*;
import cn.kinoko.model.manga.entity.*;
import cn.kinoko.model.manga.query.MangaQuery;
import cn.kinoko.model.manga.req.*;
import cn.kinoko.model.manga.vo.*;
import cn.kinoko.service.cnTeam.CnTeamMemberService;
import cn.kinoko.service.cnTeam.CnTeamService;
import cn.kinoko.service.cnTeam.PositionService;
import cn.kinoko.service.common.util.AuthUtil;
import cn.kinoko.service.common.util.ThumbPathHelper;
import cn.kinoko.service.manga.*;
import cn.kinoko.service.minio.MinioService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.time.LocalDate;
import java.time.temporal.WeekFields;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;

/**
 * @author kinoko
 */
@Slf4j
@AllArgsConstructor
@Service
public class MangaServiceImpl extends BaseServiceImpl<MangaMapper, Manga> implements MangaService {

    private MangaEpService mangaEpService;
    private MinioService minioService;
    private MangaEpCookedService mangaEpCookedService;
    private MangaEpRawService mangaEpRawService;
    private MangaMarkService mangaMarkService;
    private MangaCnMemberService mangaCnMemberService;
    private MangaMetricsService mangaMetricsService;
    private CnTeamMemberService cnTeamMemberService;
    private CnTeamService cnTeamService;
    private PositionService positionService;
    private MangaMarkCommentService mangaMarkCommentService;
    private ExecutorService executorService;

    // 批量上传的批次大小
    private final Integer BATCH_SIZE = 50;

    /**
     * 保存漫画
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean saveManga(MangaReq req) {
        Long uid = AuthUtil.getUidAndCheck();
        // 查询漫画名称是否已被占用
        boolean nameExist = lambdaQuery().eq(Manga::getName, req.getName()).exists();
        ExceptionUtil.assertion(nameExist, ErrorCodeEnum.SYSTEM_ERROR_B0403);
        // 查询汉化组
        CnTeam cnTeam = cnTeamService.getById(req.getTid());
        ExceptionUtil.isNull(cnTeam, ErrorCodeEnum.SYSTEM_ERROR_B0406);
        // 查询当前用户在组中的职位
        Long position = cnTeamMemberService.queryPosition(cnTeam.getTid(), uid);
        // 将组长转为监制
        if (BitmapUtil.isBitAt(position, Position.LEADER.getCode().intValue())) {
            position = BitmapUtil.clearBitAt(position, Position.LEADER.getCode().intValue());
            position = BitmapUtil.setBitAt(position, Position.PRODUCER.getCode().intValue());
        }
        // 组装漫画实体
        Manga manga = Manga.builder()
                .name(req.getName())
                .about(req.getAbout())
                .author(req.getAuthor())
                .tid(cnTeam.getTid())
                .cnTeam(cnTeam.getTeamName())
                .coverUrl(req.getCoverUrl())
                .status(req.getStatus())
                .isAvailable(req.getIsAvailable())
                .build();
        save(manga);
        // 添加坑组成员
        MangaCnMember mangaCnMember = new MangaCnMember();
        mangaCnMember.setMangaId(manga.getMangaId());
        mangaCnMember.setUid(uid);
        mangaCnMember.setPosition(position);
        mangaCnMemberService.save(mangaCnMember);
        // 添加封面文件的引用
        minioService.saveFileReference(Collections.singletonList(manga.getCoverUrl()));
        return true;
    }

    /**
     * 更新漫画
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean updateManga(MangaReq req) {
        Long uid = AuthUtil.getUidAndCheck();
        // 查询漫画
        Manga manga = getAndCheckManga(req.getMangaId());
        // 如果汉化组变更
        if (!manga.getTid().equals(req.getTid())) {
            // 查询汉化组
            CnTeam cnTeam = cnTeamService.getById(req.getTid());
            ExceptionUtil.isNull(cnTeam, ErrorCodeEnum.SYSTEM_ERROR_B0406);
            // 查看当前用户是否为汉化组成员
            cnTeamMemberService.queryPosition(cnTeam.getTid(), uid);
            // 更新漫画所属汉化组名称
            manga.setCnTeam(cnTeam.getTeamName());
        }
        // 如果封面更新
        if (!manga.getCoverUrl().equals(req.getCoverUrl())) {
            // 删除旧封面的引用
            minioService.deleteFileReference(Collections.singletonList(manga.getCoverUrl()));
            // 添加新封面的引用
            minioService.saveFileReference(Collections.singletonList(req.getCoverUrl()));
        }
        BeanUtils.copyProperties(req, manga);
        // 更新漫画信息
        return updateById(manga, false);
    }

    /**
     * 设置漫画是否展示
     *
     * @param mangaId     漫画id
     * @param isAvailable 是否可用
     * @return 执行结果
     */
    @Override
    public boolean available(Long mangaId, Boolean isAvailable) {
        // 查询漫画
        Manga manga = getAndCheckManga(mangaId);
        if (manga.getIsAvailable().equals(isAvailable)) {
            return true;
        }
        manga.setIsAvailable(isAvailable);
        return updateById(manga, false);
    }

    /**
     * 删除漫画
     *
     * @param mangaId 漫画id
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean deleted(Long mangaId) {
        getAndCheckManga(mangaId);
        // 删除漫画，及其相关数据
        mangaEpService.delByMangaId(mangaId);
        return removeById(mangaId);
    }

    /**
     * 分页查询
     *
     * @param query 查询参数
     * @return 分页结果
     */
    @Override
    public Page<MangaVO> homePage(BaseQuery query) {
        // 排序规则：更新时间 > 热度 > 漫画ID
        Page<Manga> page = getPage(query);
        lambdaQuery()
                .eq(Manga::getIsAvailable, Flag.YES)
                .orderByDesc(Manga::getModifyTime)
                .orderByDesc(Manga::getHeat)
                .orderByDesc(Manga::getMangaId)
                .page(page);
        return BeanHelper.copyPage(page, MangaVO.class,
                mangaVO -> ThumbPathHelper.toThumbPath(mangaVO, MangaVO::getCoverUrl, MangaVO::setThumbCoverUrl));
    }

    /**
     * 获取我参与的漫画列表
     *
     * @param query 查询参数
     * @return 漫画列表
     */
    @Override
    public Page<MangaVO> mineMange(MangaQuery query) {
        Long uid = AuthUtil.getUidAndCheck();
        query.setUid(uid);
        // 查询我参与的漫画
        Page<MangaVO> page = getBaseMapper().queryMineMangaPage(getPage(query), query);
        // 填充缩略图
        page.getRecords().forEach(mangaVO -> ThumbPathHelper.toThumbPath(mangaVO, MangaVO::getCoverUrl, MangaVO::setThumbCoverUrl));
        return page;
    }

    /**
     * 获取热度前10的漫画
     *
     * @param cycle 周期 1 年 2 月 3 周
     * @return 漫画列表
     */
    @Override
    public List<MangaVO> top10Manga(Integer cycle) {
        LocalDate now = LocalDate.now();
        return switch (cycle) {
            case 1 -> handleMangaVO(getBaseMapper().top10MangaByYear(now.getYear()));
            case 2 -> handleMangaVO(getBaseMapper().top10MangaByMonth(now.getMonthValue()));
            case 3 -> handleMangaVO(getBaseMapper().top10MangaByWeek(now.get(WeekFields.of(Locale.CHINA).weekOfWeekBasedYear())));
            default -> List.of();
        };
    }

    /**
     * 获取更新频率前10的漫画
     *
     * @param cycle 周期 1 月 2 周 3 日
     * @return 漫画列表
     */
    @Override
    public List<MangaVO> top10UpdateManga(Integer cycle) {
        LocalDate now = LocalDate.now();
        return switch (cycle) {
            case 1 -> handleMangaVO(getBaseMapper().top10UpdateMangaByMonth(now.getMonthValue()));
            case 2 ->
                    handleMangaVO(getBaseMapper().top10UpdateMangaByWeek(now.get(WeekFields.of(Locale.CHINA).weekOfWeekBasedYear())));
            case 3 -> handleMangaVO(getBaseMapper().top10UpdateMangaByDate(now));
            default -> List.of();
        };
    }

    /**
     * 搜索漫画
     *
     * @param query 查询参数
     * @return 漫画列表
     */
    @Override
    public Page<MangaSearchVO> searchManga(MangaQuery query) {
        if (StringUtils.isBlank(query.getKeyword())) {
            return new Page<>();
        }
        // 查询漫画名、组名、作者名
        Page<Manga> page = lambdaQuery()
                .eq(Manga::getIsAvailable, Flag.YES)
                .and(wrapper -> wrapper
                        .likeRight(Manga::getName, query.getKeyword()).or()
                        .likeRight(Manga::getAuthor, query.getKeyword()).or()
                        .likeRight(Manga::getCnTeam, query.getKeyword()))
                .page(getPage(query));
        // 填充缩略图
        return BeanHelper.copyPage(page, MangaSearchVO.class,
                mangaVO -> ThumbPathHelper.toThumbPath(mangaVO, MangaSearchVO::getCoverUrl, MangaSearchVO::setThumbCoverUrl));
    }

    /**
     * 获取漫画信息
     *
     * @param mangaId 漫画id
     * @return 漫画信息
     */
    @Override
    public MangaInfoVO info(Long mangaId) {
        // 查询漫画
        Manga manga = getAndCheckManga(mangaId);
        MangaInfoVO mangaInfoVO = BeanHelper.copyProperties(manga, MangaInfoVO.class);
        ThumbPathHelper.toThumbPath(mangaInfoVO, MangaInfoVO::getCoverUrl, MangaInfoVO::setThumbCoverUrl);
        return mangaInfoVO;
    }

    /**
     * 获取漫画的章节列表
     *
     * @param mangaId 漫画id
     * @return 章节列表
     */
    @Override
    public List<MangaEpVO> epList(Long mangaId) {
        // 查询漫画
        getAndCheckManga(mangaId);
        // 返回章节列表
        return mangaEpService.lambdaQuery()
                .eq(MangaEp::getMangaId, mangaId)
                .orderByAsc(MangaEp::getNumber)
                .list().parallelStream()
                .map(mangaEp -> {
                    MangaEpVO mangaEpVO = BeanHelper.copyProperties(mangaEp, MangaEpVO.class);
                    assert mangaEpVO != null;
                    mangaEpVO.setMarkStatus(EnumUtils.getEnum(EpMarkStatus.class, mangaEp.getMarkStatus()).getDesc());
                    mangaEpVO.setCutoutStatus(EnumUtils.getEnum(EpCutoutStatus.class, mangaEp.getCutoutStatus()).getDesc());
                    return mangaEpVO;
                }).toList();
    }

    /**
     * 新增章节
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean saveEp(MangaEpReq req) {
        Manga manga = getAndCheckManga(req.getMangaId());
        // 查询最新话数
        MangaEp lastEp = mangaEpService.lambdaQuery()
                .select(MangaEp::getNumber)
                .eq(MangaEp::getMangaId, manga.getMangaId())
                .orderByDesc(MangaEp::getNumber)
                .last("limit 1")
                .one();
        int lastNumber = lastEp == null ? 1 : lastEp.getNumber() + 1;
        // 组装章节实体
        MangaEp mangaEp = new MangaEp() {{
            setMangaId(manga.getMangaId());
            setNumber(lastNumber);
            setName(req.getName() == null ? getNumber().toString() : req.getName());
            setMarkStatus(EpMarkStatus.NOT_APPLY.getCode());
            setCutoutStatus(EpCutoutStatus.NOT_APPLY.getCode());
        }};
        // 保存章节
        mangaEpService.save(mangaEp);
        // 待烤章节数 + 1
        manga.setRawEpCount(manga.getRawEpCount() + 1);
        return updateById(manga, false);
    }

    /**
     * 更新章节
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @Override
    public boolean epUpdate(MangaEpReq req) {
        getAndCheckManga(req.getMangaId());
        MangaEp ep = mangaEpService.getById(req.getEpId());
        ExceptionUtil.isNull(ep, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        ep.setName(req.getName());
        return mangaEpService.updateById(ep);
    }

    /**
     * 删除章节
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean epDeleted(MangaEpReq req) {
        Manga manga = getAndCheckManga(req.getMangaId());
        MangaEp ep = mangaEpService.getById(req.getEpId());
        ExceptionUtil.isNull(ep, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        // 删除漫画章节
        mangaEpService.lambdaUpdate().eq(MangaEp::getEpId, req.getEpId()).remove();
        // 删除漫画翻译
        mangaMarkService.lambdaUpdate().eq(MangaMark::getEpId, req.getEpId()).remove();
        // 删除生肉
        mangaEpRawService.delByEpId(req.getEpId());
        // 删除熟肉
        mangaEpCookedService.delByEpId(req.getEpId());
        // 生肉 - 1
        manga.setRawEpCount(manga.getRawEpCount() - 1);
        // 回退最新章节到最新熟肉
        rollbackLastEp2LastCooked(manga);
        return true;
    }

    /**
     * 章节排序
     *
     * @param req 排序参数
     * @return 执行结果
     */
    @Override
    public boolean epReSort(MangaEpReSortReq req) {
        getAndCheckManga(req.getMangaId());
        // 更新排序
        List<MangaEp> reSortList = req.getSortList().stream().map(item -> {
            MangaEp mangaEp = new MangaEp();
            mangaEp.setEpId(item.getEpId());
            mangaEp.setNumber(item.getNumber());
            return mangaEp;
        }).toList();
        // 更新排序
        mangaEpService.updateBatchById(reSortList);
        return true;
    }

    /**
     * 设置章节是否展示
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @CacheEvict(key = CacheKeyConstant.MG_EP_LIST, params = "#req.mangaId")
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean epAvailable(MangaEpAvailableReq req) {
        Manga manga = getAndCheckManga(req.getMangaId());
        MangaEp mangaEp = mangaEpService.getById(req.getEpId());
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        if (mangaEp.getIsAvailable().equals(req.getIsAvailable())) {
            return true;
        }
        // 更新ep展示状态
        mangaEp.setIsAvailable(req.getIsAvailable());
        mangaEpService.updateById(mangaEp);
        // 当设置为展示时，更新漫画的章节总数和最新话，否则回退
        if (req.getIsAvailable()) {
            // 判断当前ep是否存在熟肉
            boolean havCooked = mangaEpCookedService.lambdaQuery()
                    .eq(MangaEpCooked::getMangaId, req.getMangaId())
                    .eq(MangaEpCooked::getEpId, req.getEpId())
                    .exists();
            ExceptionUtil.assertion(!havCooked, ErrorCodeEnum.SYSTEM_ERROR_B0404);
            // 漫画熟肉 + 1 更新最新章节数
            manga.setEpCount(manga.getEpCount() + 1);
            manga.setLastEpName(mangaEp.getName());
            // 更新漫画主体
            updateById(manga);
        } else {
            // 回退最新章节到最新熟肉
            rollbackLastEp2LastCooked(manga);
        }
        return true;
    }

    /**
     * 获取漫画生肉列表
     *
     * @param epId 章节id
     * @return 生肉列表
     */
    @Override
    public List<MangaEpRawVO> rawList(Long epId) {
        MangaEp ep = mangaEpService.getById(epId);
        ExceptionUtil.isNull(ep, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        // 查询组装并返回
        return mangaEpRawService.lambdaQuery()
                .eq(MangaEpRaw::getEpId, epId)
                .orderByAsc(MangaEpRaw::getNumber)
                .list().stream()
                .map(mangaRaw -> {
                    MangaEpRawVO mangaEpRawVO = BeanHelper.copyProperties(mangaRaw, MangaEpRawVO.class);
                    ThumbPathHelper.toThumbPath(mangaEpRawVO, MangaEpImgVO::getImageUrl, MangaEpImgVO::setThumbUrl);
                    return mangaEpRawVO;
                })
                .collect(Collectors.toList());
    }

    /**
     * 获取漫画熟肉列表
     *
     * @param epId 章节id
     * @return 熟肉列表
     */
    @Override
    public List<MangaEpCookedVO> cookedList(Long epId) {
        MangaEp ep = mangaEpService.getById(epId);
        ExceptionUtil.isNull(ep, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        // 查询组装并返回
        return mangaEpCookedService.queryList(epId);
    }

    /**
     * 上传漫画的原始文件
     *
     * @param epId  章节id
     * @param files 文件
     * @return 执行结果
     */
    @Transactional(rollbackFor = RuntimeException.class)
    @Override
    public boolean uploadRawMg(Long epId, MultipartFile[] files) throws InterruptedException {
        ExceptionUtil.assertion(files == null || files.length == 0, "请选择上传文件");
        MangaEp mangaEp = mangaEpService.getById(epId);
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        Manga manga = getAndCheckManga(mangaEp.getMangaId());
        // 查询当前章节生肉的最后一页
        MangaEpRaw lastPage = mangaEpRawService.lambdaQuery()
                .select(MangaEpRaw::getNumber)
                .eq(MangaEpRaw::getEpId, epId)
                .orderByDesc(MangaEpRaw::getNumber)
                .last("limit 1")
                .one();
        int startNumber = 0;
        if (lastPage != null) {
            // 找到最后一页获取页码
            startNumber = lastPage.getNumber();
        }
        // 上传文件
        Map<Integer, String> filePathMap = uploadMg(files, manga.getMangaId(), mangaEp.getNumber(), true);
        // 落库
        try {
            if (!filePathMap.isEmpty()) {
                // 校验上传文件数量
                if (filePathMap.size() != files.length) {
                    throw new RuntimeException("file total count not match");
                }
                int finalStartNumber = startNumber;
                List<MangaEpRaw> rawList = filePathMap.entrySet().stream()
                        .map(entry -> new MangaEpRaw() {{
                            setEpId(mangaEp.getEpId());
                            setMangaId(manga.getMangaId());
                            setNumber(finalStartNumber + entry.getKey());
                            setImageUrl(entry.getValue());
                        }}).collect(Collectors.toList());
                // 批量保存页码
                mangaEpRawService.saveBatch(rawList);
                // 保存文件引用
                minioService.saveFileReference(filePathMap.values());
            }
            return true;
        } catch (Exception e) {
            log.error("[漫画上传] 上传失败：", e);
            // 回滚文件
            rollbackFile(filePathMap);
            return false;
        }
    }

    /**
     * 重传生肉
     *
     * @param epId   章节id
     * @param pageId 页码id
     * @param file   文件
     * @return 执行结果
     */
    @Override
    public boolean reUploadRawMg(Long epId, Long pageId, MultipartFile file) throws InterruptedException {
        MangaEp mangaEp = mangaEpService.getById(epId);
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        MangaEpRaw epRaw = mangaEpRawService.getById(pageId);
        ExceptionUtil.isNull(epRaw, ErrorCodeEnum.SYSTEM_ERROR_B0405);
        // epRaw
        Map<Integer, String> filePathMap = uploadMg(new MultipartFile[]{file}, epRaw.getMangaId(), mangaEp.getNumber(), false);
        if (!filePathMap.isEmpty()) {
            // 取消引用旧文件
            minioService.deleteFileReference(Collections.singletonList(epRaw.getImageUrl()));
            // 更新页码图片
            filePathMap.values().stream().findFirst().ifPresent(epRaw::setImageUrl);
            return mangaEpRawService.updateById(epRaw);
        }
        return false;
    }

    /**
     * 上传漫画熟肉
     *
     * @param epId  章节id
     * @param files 文件
     * @return 执行结果
     */
    @Transactional(rollbackFor = RuntimeException.class)
    @Override
    public boolean uploadCookedMg(Long epId, MultipartFile[] files) throws InterruptedException {
        ExceptionUtil.assertion(files == null || files.length == 0, "请选择上传文件");
        MangaEp mangaEp = mangaEpService.getById(epId);
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        Manga manga = getAndCheckManga(mangaEp.getMangaId());
        // 检查当前章节下的生肉页码是否达到阈值
        Long pageCount = mangaEpRawService.lambdaQuery().eq(MangaEpRaw::getEpId, epId).count();
        ExceptionUtil.assertion(pageCount >= 250, ErrorCodeEnum.SYSTEM_ERROR_B0413);

        // 查询当前章节生肉的最后一页
        MangaEpCooked lastPage = mangaEpCookedService.lambdaQuery()
                .select(MangaEpCooked::getNumber)
                .eq(MangaEpCooked::getEpId, epId)
                .orderByDesc(MangaEpCooked::getNumber)
                .last("limit 1")
                .one();
        int startNumber = 0;
        if (lastPage != null) {
            // 找到最后一页获取页码
            startNumber = lastPage.getNumber();
        }
        // 上传文件
        Map<Integer, String> filePathMap = uploadMg(files, manga.getMangaId(), mangaEp.getNumber(), false);
        // 落库
        try {
            if (!filePathMap.isEmpty()) {
                // 校验上传文件数量
                if (filePathMap.size() != files.length) {
                    throw new RuntimeException("file total count not match");
                }
                int finalStartNumber = startNumber;
                List<MangaEpCooked> cookedList = filePathMap.entrySet().stream()
                        .map(entry -> new MangaEpCooked() {{
                            setEpId(mangaEp.getEpId());
                            setMangaId(manga.getMangaId());
                            setNumber(finalStartNumber + entry.getKey());
                            setImageUrl(entry.getValue());
                        }}).collect(Collectors.toList());
                mangaEpCookedService.saveBatch(cookedList);
                // 保存文件引用
                minioService.saveFileReference(filePathMap.values());
            }
            return true;
        } catch (Exception e) {
            log.error("[漫画上传] 上传失败：", e);
            // 回滚文件
            rollbackFile(filePathMap);
            return false;
        }
    }

    /**
     * 重传熟肉
     *
     * @param epId   章节id
     * @param pageId 页码id
     * @param file   文件
     * @return 执行结果
     */
    @Override
    public boolean reUploadCookedMg(Long epId, Long pageId, MultipartFile file) throws InterruptedException {
        MangaEp mangaEp = mangaEpService.getById(epId);
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        MangaEpCooked epCooked = mangaEpCookedService.getById(pageId);
        ExceptionUtil.isNull(epCooked, ErrorCodeEnum.SYSTEM_ERROR_B0405);
        // 检查当前章节下的熟肉页码是否达到阈值
        Long pageCount = mangaEpCookedService.lambdaQuery().eq(MangaEpCooked::getEpId, epId).count();
        ExceptionUtil.assertion(pageCount >= 250, ErrorCodeEnum.SYSTEM_ERROR_B0413);

        // 上传新文件
        Map<Integer, String> filePathMap = uploadMg(new MultipartFile[]{file}, epCooked.getMangaId(), mangaEp.getNumber(), false);
        if (!filePathMap.isEmpty()) {
            // 取消引用旧文件
            minioService.deleteFileReference(Collections.singletonList(epCooked.getImageUrl()));
            // 更新页码图片
            filePathMap.values().stream().findFirst().ifPresent(epCooked::setImageUrl);
            mangaEpCookedService.updateById(epCooked);
            return true;
        }
        return false;
    }

    /**
     * 删除生肉页码
     *
     * @param epId   章节id
     * @param pageId 页码id
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean deletedRaw(Long epId, Long pageId) {
        MangaEp mangaEp = mangaEpService.getById(epId);
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        // 查询生肉页码
        MangaEpRaw epRaw = mangaEpRawService.getById(pageId);
        // 取消文件引用
        minioService.deleteFileReference(Collections.singletonList(epRaw.getImageUrl()));
        // 删除页码
        return mangaEpRawService.delById(pageId);
    }

    /**
     * 清空生肉页码
     *
     * @param epId 章节id
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean clearRaw(Long epId) {
        MangaEp mangaEp = mangaEpService.getById(epId);
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        // 查询生肉页码
        List<String> rawImgList = mangaEpRawService.lambdaQuery().eq(MangaEpRaw::getEpId, epId)
                .list().parallelStream().map(MangaEpRaw::getImageUrl).toList();
        // 取消文件引用
        minioService.deleteFileReference(rawImgList);
        // 清空生肉页码
        mangaEpRawService.delByEpId(epId);
        return true;
    }

    /**
     * 删除熟肉页码
     *
     * @param epId   章节id
     * @param pageId 页码id
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean deletedCooked(Long epId, Long pageId) {
        MangaEp mangaEp = mangaEpService.getById(epId);
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        MangaEpCooked epCooked = mangaEpCookedService.getById(pageId);
        ExceptionUtil.isNull(epCooked, ErrorCodeEnum.SYSTEM_ERROR_B0405);
        // 删除页码
        mangaEpCookedService.removeById(pageId);
        // 删除页码引用
        minioService.deleteFileReference(Collections.singletonList(epCooked.getImageUrl()));
        // 判断是否已经没有生肉
        boolean existsCooked = mangaEpCookedService.lambdaQuery()
                .eq(MangaEpCooked::getEpId, epId)
                .exists();
        if (!existsCooked) {
            // 将烹饪状态置为生肉
            MangaEpAvailableReq availableReq = new MangaEpAvailableReq();
            availableReq.setIsAvailable(false);
            availableReq.setMangaId(mangaEp.getMangaId());
            availableReq.setEpId(epId);
            // 获取当前代理对象调用
            ((MangaService) AopContext.currentProxy()).epAvailable(availableReq);
        }
        return true;
    }

    /**
     * 清空熟肉页码
     *
     * @param epId 章节id
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean clearCooked(Long epId) {
        MangaEp mangaEp = mangaEpService.getById(epId);
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        // 查询熟肉页码
        List<String> cookedImgList = mangaEpCookedService.lambdaQuery().eq(MangaEpCooked::getEpId, epId)
                .list().parallelStream().map(MangaEpCooked::getImageUrl).toList();
        // 取消文件引用
        minioService.deleteFileReference(cookedImgList);
        // 清空熟肉页码
        mangaEpCookedService.delByEpId(epId);
        // 将烹饪状态置为生肉
        MangaEpAvailableReq availableReq = new MangaEpAvailableReq();
        availableReq.setIsAvailable(false);
        availableReq.setMangaId(mangaEp.getMangaId());
        availableReq.setEpId(epId);
        // 获取当前代理对象调用
        ((MangaService) AopContext.currentProxy()).epAvailable(availableReq);
        return true;
    }

    /**
     * 重新排序生肉页码
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean reSortRawList(MangaPageReSortReq req) {
        MangaEp mangaEp = mangaEpService.getById(req.getEpId());
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        // 组装排序对象
        List<MangaEpRaw> reSortList = req.getSortList().stream().map(item -> {
            MangaEpRaw mangaEpRaw = new MangaEpRaw();
            mangaEpRaw.setEpRid(item.getPageId());
            mangaEpRaw.setNumber(item.getNumber());
            return mangaEpRaw;
        }).toList();
        return mangaEpRawService.updateBatchById(reSortList);
    }

    /**
     * 重新排序熟肉页码
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean reSortCookedList(MangaPageReSortReq req) {
        MangaEp mangaEp = mangaEpService.getById(req.getEpId());
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        // 组装排序对象
        List<MangaEpCooked> reSortList = req.getSortList().stream().map(item -> {
            MangaEpCooked mangaEpCooked = new MangaEpCooked();
            mangaEpCooked.setEpCid(item.getPageId());
            mangaEpCooked.setNumber(item.getNumber());
            return mangaEpCooked;
        }).toList();
        return mangaEpCookedService.updateBatchById(reSortList);
    }

    /**
     * 保存标注
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @SneakyThrows
    @Transactional(rollbackFor = RuntimeException.class)
    @Override
    public boolean saveMark(MarkReq req) {
        Long uid = AuthUtil.getUidAndCheck();
        MangaEpRaw mangaRaw = mangaEpRawService.getById(req.getEpRid());
        ExceptionUtil.isNull(mangaRaw, ErrorCodeEnum.SYSTEM_ERROR_B0402);
        // 查询当前页的标注id
        Map<Long, MangaMark> markMap = mangaMarkService.lambdaQuery()
                .eq(MangaMark::getEpRid, req.getEpRid())
                .list().parallelStream().collect(Collectors.toMap(MangaMark::getMarkId, item -> item));
        // 分出增删改的标注
        CUDHelper<Long, MarkReq.Mark> cudHelper = CUDHelper.<Long, MarkReq.Mark>builder()
                .dbIds(markMap.keySet())
                .reqList(req.getMarkList())
                .idMapper(MarkReq.Mark::getMarkId)
                .build();
        // 更新标注
        updateMark(cudHelper, markMap, uid);
        // 删除标注
        delMark(cudHelper);
        // 新增标注
        saveMark(cudHelper, mangaRaw, uid);
        return true;
    }

    /**
     * 新增标注
     *
     * @param cudHelper cud辅助类
     * @param mangaRaw  生肉章节
     * @param uid       用户id
     */
    private void saveMark(CUDHelper<Long, MarkReq.Mark> cudHelper, MangaEpRaw mangaRaw, Long uid) {
        // 添加的标注
        List<MarkReq.Mark> addMarkList = cudHelper.getNewItem();
        // 新增标注
        if (!addMarkList.isEmpty()) {
            List<MangaMark> newMarkList = addMarkList.parallelStream().map(mark -> {
                MangaMark newMark = BeanHelper.copyProperties(mangaRaw, MangaMark.class);
                assert newMark != null;
                newMark.setEpId(mangaRaw.getEpId());
                newMark.setNumber(mark.getNumber());
                newMark.setText(mark.getText());
                if (StringUtils.isNotBlank(mark.getTextAmend())) {
                    newMark.setTextAmend(mark.getTextAmend());
                    newMark.setProofreader(uid);
                }
                newMark.setX(mark.getX());
                newMark.setY(mark.getY());
                return newMark;
            }).toList();
            // 标注落库
            mangaMarkService.saveBatch(newMarkList);
        }
    }

    /**
     * 构建标注留言对象
     * @param comment 留言
     * @param mark 标注
     * @return 留言对象
     */
    private static @NotNull MangaMarkComment buildMarkComment(String comment, MangaMark mark) {
        MangaMarkComment markComment = new MangaMarkComment();
        markComment.setMangaId(mark.getMangaId());
        markComment.setEpId(mark.getEpId());
        markComment.setEpRid(mark.getEpRid());
        markComment.setMarkId(mark.getMarkId());
        markComment.setComment(comment);
        return markComment;
    }

    /**
     * 删除标注
     *
     * @param cudHelper cud辅助类
     */
    private void delMark(CUDHelper<Long, MarkReq.Mark> cudHelper) {
        // 删除的标注
        List<Long> delMarkIdList = cudHelper.getDelItemId();
        // 删除标注
        if (!delMarkIdList.isEmpty()) {
            mangaMarkService.delByIds(delMarkIdList);
        }
    }

    /**
     * 更新标注
     *
     * @param cudHelper cud辅助类
     * @param markMap   标注map
     * @param uid       用户id
     */
    private void updateMark(CUDHelper<Long, MarkReq.Mark> cudHelper, Map<Long, MangaMark> markMap, Long uid) {
        // 修改的标注
        List<MarkReq.Mark> updateMarkList = cudHelper.getUpdateItem();
        // 更新的标注
        if (!updateMarkList.isEmpty()) {
            List<MangaMark> updateList = updateMarkList.parallelStream()
                    .filter(updateReq -> markMap.containsKey(updateReq.getMarkId()))
                    .map(updateReq -> {
                        MangaMark updateMark = markMap.get(updateReq.getMarkId());
                        // 比较校对文本是否被修改，是则更新校对人
                        if (!Objects.equals(updateReq.getTextAmend(), updateMark.getTextAmend())) {
                            updateMark.setProofreader(uid);
                        }
                        BeanUtils.copyProperties(updateReq, updateMark);
                        return updateMark;
                    }).toList();
            mangaMarkService.updateBatchById(updateList);
        }
    }

    /**
     * 获取标注列表
     *
     * @param epRid 章节生肉id
     * @return 标注列表
     */
    @Override
    public List<MarkVO> markList(Long epRid) {
        List<MangaMark> markList = mangaMarkService.lambdaQuery()
                .select(MangaMark::getMarkId,
                        MangaMark::getNumber,
                        MangaMark::getText,
                        MangaMark::getTextAmend,
                        MangaMark::getX,
                        MangaMark::getY,
                        MangaMark::getProofreader,
                        MangaMark::getNotice,
                        MangaMark::getCreateBy)
                .eq(MangaMark::getEpRid, epRid)
                .orderByAsc(MangaMark::getNumber)
                .list();
        return BeanHelper.copyWithCollection(markList, MarkVO.class);
    }

    /**
     * 根据汉化组id热度前5的漫画
     *
     * @param tId 汉化组id
     * @return 热度前5的漫画
     */
    @Override
    public List<MangaCoverVO> queryTop5MangaByTid(Long tId) {
        return getBaseMapper().queryTop5MangaByTid(tId).stream()
                .peek(mangaCoverVO -> ThumbPathHelper.toThumbPath(mangaCoverVO, MangaCoverVO::getCoverUrl, MangaCoverVO::setThumbCoverUrl))
                .toList();
    }

    /**
     * 漫画成员列表
     *
     * @param mangaId 漫画id
     * @return 成员列表
     */
    @Cacheable(key = CacheKeyConstant.MG_MEMBER_LIST, params = "#mangaId")
    @Override
    public List<MemberVO> mangaMember(Long mangaId) {
        getAndCheckManga(mangaId);
        // 查询成员列表
        return mangaCnMemberService.queryMangaMember(mangaId);
    }

    /**
     * 漫画成员列表
     *
     * @param epId 章节id
     * @return 成员列表
     */
    @Override
    public List<MemberVO> mangaMemberByEpId(Long epId) {
        // 查询漫画id
        MangaEp mangaEp = mangaEpService.getById(epId);
        ExceptionUtil.isNull(mangaEp, ErrorCodeEnum.SYSTEM_ERROR_B0401);
        // 通过代理调用走aop
        return ((MangaService) AopContext.currentProxy()).mangaMember(mangaEp.getMangaId());
    }

    /**
     * 漫画成员职位列表
     *
     * @return 职位列表
     */
    @Override
    public List<KV<Long, String>> positionList() {
        return positionService.queryAll(PositionType.MANGA.getCode()).entrySet().stream().map(e -> new KV<>(e.getKey(), e.getValue())).toList();
    }

    /**
     * 漫画成员职位变更
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @Override
    public boolean changePosition(MangaMemberPositionReq req) {
        // 查询汉化组
        getAndCheckManga(req.getMangaId());
        // 查看是否有该职位
        Set<Long> allPositionId = positionService.queryAll(PositionType.MANGA.getCode()).keySet();
        boolean unknownPosition = req.getPositionIds().stream().anyMatch(id -> !allPositionId.contains(id));
        ExceptionUtil.assertion(unknownPosition, "存在未知职位");
        // 查询是否加入当前漫画坑组
        MangaCnMember member = mangaCnMemberService.lambdaQuery()
                .eq(MangaCnMember::getUid, req.getUid())
                .eq(MangaCnMember::getMangaId, req.getMangaId())
                .list().stream().findFirst().orElse(null);
        ExceptionUtil.isNull(member, "未找到该成员~");
        // 更新漫画坑组职位
        long position = BitmapUtil.setBitAt(0, req.getPositionIds());
        member.setPosition(position);
        return mangaCnMemberService.updateById(member);
    }

    /**
     * 添加漫画成员
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean appendMember(AppendMemberReq req) {
        Long mangaId = req.getMangaId();
        // 检查漫画是否存在
        Manga manga = getAndCheckManga(mangaId);
        // 查询当前汉化组成员
        Map<Long, Long> teamMemberMap = cnTeamMemberService.lambdaQuery().eq(CnTeamMember::getTId, manga.getTid()).list()
                .stream().collect(Collectors.toMap(CnTeamMember::getUid, CnTeamMember::getPosition));
        // 判断添加的成员是否为汉化组成员
        ExceptionUtil.assertion(req.getMemberList().stream().anyMatch(uid -> !teamMemberMap.containsKey(uid)), "添加成员中有非汉化组成员哦~");
        // 查询当前坑组成员
        List<Long> currentMembers = mangaCnMemberService.lambdaQuery().select(MangaCnMember::getUid).eq(MangaCnMember::getMangaId, mangaId).list()
                .stream().map(MangaCnMember::getUid).toList();
        // 获取漫画职位
        Set<Long> positionList = positionService.queryAll(PositionType.MANGA.getCode()).keySet();
        // 新加入的成员
        List<Long> appendMembers = req.getMemberList().stream().filter(uid -> !currentMembers.contains(uid)).toList();
        if (!appendMembers.isEmpty()) {
            List<MangaCnMember> addList = appendMembers.stream()
                    .map(uid -> {
                        // 汉化组职位转漫画坑组职位
                        long position = handleTeamPosition2MangaPosition(teamMemberMap.get(uid), positionList);
                        MangaCnMember mangaCnMember = new MangaCnMember();
                        mangaCnMember.setPosition(position);
                        mangaCnMember.setUid(uid);
                        mangaCnMember.setMangaId(mangaId);
                        return mangaCnMember;
                    }).toList();
            mangaCnMemberService.saveBatch(addList);
        }
        // 移除的成员
        List<Long> removeMembers = currentMembers.stream().filter(uid -> !req.getMemberList().contains(uid)).toList();
        if (!removeMembers.isEmpty()) {
            mangaCnMemberService.lambdaUpdate()
                    .in(MangaCnMember::getUid, removeMembers)
                    .eq(MangaCnMember::getMangaId, mangaId)
                    .remove();
        }
        return true;
    }

    /**
     * 移除漫画成员
     *
     * @param mangaId 漫画id
     * @param uid     成员id
     * @return 执行结果
     */
    @Override
    public boolean removeMember(Long mangaId, Long uid) {
        Manga manga = getAndCheckManga(mangaId);
        // 判断如果是漫画的创建人则不允许移除
        ExceptionUtil.assertion(manga.getCreateBy().equals(uid), "不能移除坑组创建人哦~");
        // 移除成员
        return mangaCnMemberService.lambdaUpdate()
                .eq(MangaCnMember::getUid, uid)
                .eq(MangaCnMember::getMangaId, mangaId)
                .remove();
    }

    /**
     * 漫画浏览量+1
     *
     * @param mangaId 漫画id
     * @param request 请求
     * @return 执行结果
     */
    @Override
    public boolean plusView(Long mangaId, HttpServletRequest request) {
        Long uid = AuthUtil.getUid();
        // 增加浏览量
        mangaMetricsService.metricsPlus(mangaId, uid == null ? request.getRequestedSessionId() : uid, MetricsType.VIEW);
        return true;
    }

    /**
     * 漫画点赞
     *
     * @param mangaId 漫画id
     * @param isLike  是否点赞 true 点赞 false 取消点赞
     * @return 执行结果
     */
    @Override
    public boolean like(Long mangaId, Boolean isLike) {
        Long uid = AuthUtil.getUidAndCheck();
        if (isLike) {
            // 增加浏览量
            mangaMetricsService.metricsPlus(mangaId, uid, MetricsType.LIKE);
        } else {
            // 减少浏览量
            mangaMetricsService.metricsMinus(mangaId, uid, MetricsType.LIKE);
        }
        return true;
    }

    /**
     * 漫画点赞数量
     *
     * @param mangaId 漫画id
     * @return 执行结果
     */
    @Override
    public LikeCountVO likeCount(Long mangaId) {
        return mangaMetricsService.getLikeCountAndFlag(mangaId, AuthUtil.getUid());
    }

    /**
     * 漫画热度计算
     */
    @Override
    public void computeMangaHeat() {
        getBaseMapper().syncMangaHeat();
    }

    /**
     * 上传漫画的文件
     *
     * @param files   文件
     * @param mangaId 漫画id
     * @param ep      章节
     * @param isRaw   是否是生肉
     * @return 执行结果
     */
    private Map<Integer, String> uploadMg(MultipartFile[] files, Long mangaId, Integer ep, boolean isRaw) throws InterruptedException {
        // 用于存储上传成功的页码和地址
        Map<Integer, String> successMap = Maps.newConcurrentMap();
        // 用于存储上传失败的页码和地址
        Set<Integer> failList = Sets.newHashSet();
        // 默认按照文件上传顺序排序，异步优化批量上传，一批次10p
        int totalCount = files.length;
        int totalPages = PageHelper.getTotalPages(totalCount, BATCH_SIZE);
        CountDownLatch latch = new CountDownLatch(totalPages);
        for (int page = 1; page <= totalPages; page++) {
            int pageNumber = page;
            CompletableFuture.runAsync(() -> {
                int startIndex = PageHelper.getStartIndex(pageNumber, BATCH_SIZE);
                int endIndex = PageHelper.getEndIndex(startIndex, BATCH_SIZE, totalCount);
                try {
                    while (startIndex <= endIndex) {
                        MultipartFile file = files[startIndex++];
                        // 上传文件
                        String path = minioService.uploadFile(mangaId + "/" + ep + (isRaw ? "/raw" : "/cooked"),
                                file.getOriginalFilename(),
                                file.getContentType(),
                                file.getInputStream(),
                                false);
                        successMap.put(startIndex, path);
                    }
                } catch (Exception e) {
                    log.error("[漫画上传] 文件上传异常：", e);
                    failList.add(startIndex);
                } finally {
                    latch.countDown();
                }
            }, executorService);
        }
        // 阻塞等待上传
        latch.await();
        // 判断是否存在失败上传的文件
        if (!failList.isEmpty()) {
            rollbackFile(successMap);
        }
        return successMap;
    }

    /**
     * 回滚上传的文件
     *
     * @param successMap 上传成功的文件
     */
    private void rollbackFile(Map<Integer, String> successMap) {
        // 回滚上传文件
        minioService.deleteFiles(successMap.values());
        // 清空结果
        successMap.clear();
        ExceptionUtil.throwOut(ErrorCodeEnum.USER_ERROR_A0700);
    }

    /**
     * 获取漫画实体并校验
     *
     * @param mangaId 漫画id
     * @return 漫画实体
     */
    private @NotNull Manga getAndCheckManga(Long mangaId) {
        Manga manga = getById(mangaId);
        ExceptionUtil.isNull(manga, ErrorCodeEnum.SYSTEM_ERROR_B0400);
        return manga;
    }

    /**
     * 回退最新章节到最新熟肉
     *
     * @param manga 漫画
     */
    private void rollbackLastEp2LastCooked(Manga manga) {
        // 查询最新熟肉
        MangaEp lastCooked = mangaEpService.lambdaQuery()
                .eq(MangaEp::getMangaId, manga.getMangaId())
                .eq(MangaEp::getIsAvailable, Flag.YES)
                .orderByDesc(MangaEp::getNumber)
                .last("limit 1")
                .one();
        // 回退漫画的章节总数和最新话
        if (lastCooked != null) {
            manga.setEpCount(manga.getEpCount() - 1);
            manga.setLastEpName(lastCooked.getName());
        } else {
            // 将最新话置空
            manga.setLastEpName(null);
            manga.setEpCount(0);
        }
        // 更新漫画主体
        updateById(manga, false);
    }

    /**
     * 组内职位转漫画坑组职位
     *
     * @param teamPosition   组内职位
     * @param mangaPositions 漫画坑组职位集合
     * @return 漫画坑组职位
     */
    private long handleTeamPosition2MangaPosition(Long teamPosition, Set<Long> mangaPositions) {
        long position = 0;
        // 漫画职位与组内职位取交集
        for (Long mangaPosition : mangaPositions) {
            if (BitmapUtil.isBitAt(teamPosition, mangaPosition.intValue())) {
                position = BitmapUtil.setBitAt(position, mangaPosition.intValue());
            }
        }
        // 若无交集，则默认为翻译
        if (position == 0) {
            position = BitmapUtil.setBitAt(position, Position.TRANSLATOR.getCode().intValue());
        }
        return position;
    }

    /**
     * 处理漫画视图对象
     *
     * @param voList 漫画列表
     * @return 处理后的集合
     */
    private List<MangaVO> handleMangaVO(List<MangaVO> voList) {
        voList.forEach(mangaVO -> ThumbPathHelper.toThumbPath(mangaVO, MangaVO::getCoverUrl, MangaVO::setThumbCoverUrl));
        return voList;
    }

    /**
     * 漫画标注留言
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean comment(MarkCommentReq req) {
        // 查询标记
        MangaMark mark = mangaMarkService.getById(req.getMarkId());
        ExceptionUtil.isNull(mark, ErrorCodeEnum.SYSTEM_ERROR_B0407);
        mark.setNotice(true);
        mangaMarkService.updateById(mark);
        // 组装留言入库
        return mangaMarkCommentService.save(buildMarkComment(req.getComment(), mark));
    }

    /**
     * 漫画标注留言撤销
     *
     * @param id 标注留言id
     * @return 执行结果
     */
    @Override
    public boolean undoComment(Long id) {
        return mangaMarkCommentService.undoComment(id);
    }

    /**
     * 漫画标注留言查询
     *
     * @param markId 标注id
     * @return 执行结果
     */
    @Override
    public List<MarkCommentVO> queryComment(Long markId) {
        return mangaMarkCommentService.queryComment(markId);
    }

    /**
     * 漫画导出
     *
     * @param params 请求参数
     */
    @Override
    public boolean export(MangaExportReq params) {
        return mangaEpService.export(params);
    }

    /**
     * 漫画导出文本
     *
     * @param epId     章节id
     * @param response 响应
     */
    @Override
    public void exportText(Long epId, HttpServletResponse response) {
        mangaEpService.exportText(epId, response);
    }
}
